package de.szut.pongsim.physics;

import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

import de.szut.pongsim.ai.User;
import de.szut.pongsim.gui.Field;

public class Model {
	
	private static final int HALFTIME_ROUNDS = 5;
	private static final HorizontalDirection START_DIRECTION = HorizontalDirection.LEFT;
	private static final int START_SPEED = 1;
	private static final int AI_TIMEOUT_MS = 200;
	
	private Field field;
	private User leftUser = null;
	private User rightUser = null;
	
	public Model(Field field) {
		this.field = field;
	}
	
	private void move(Board board, Future<PadMovement> movFuture) {
		if (!movFuture.isCancelled()) {
			try {
				PadMovement movement = movFuture.get();
				movement = board.moveLeftPad(movement);
				field.moveLeftPad(movement);
			} catch (ExecutionException e) {
				e.printStackTrace();
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
	
	private void movePads(Board board, Collection<Callable<PadMovement>> movementTasks) {
		ExecutorService executor = Executors.newSingleThreadExecutor();
		try {
			List<Future<PadMovement>> movements = executor.invokeAll(movementTasks, AI_TIMEOUT_MS, TimeUnit.MILLISECONDS);
			// TODO: Test if this works
			executor.shutdownNow();
			Future<PadMovement> leftFuture = movements.get(0);
			move(board, leftFuture);
			Future<PadMovement> rightFuture = movements.get(1);
			move(board, rightFuture);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	
	private HorizontalDirection startRound(HorizontalDirection startDirection) {
		HorizontalDirection direction = startDirection;
		Board board = Board.createDefaultBoard(direction);
		int ballSpeed = START_SPEED;
		Point startPos = board.getBallPos();
		BallFunction func = BallFunction.createAtRandomAngle(startPos);
		field.respawn(startPos);
		while (true) {
			int leftPadBottomY = board.getLeftPadBottomY();
			int rightPadBottomY = board.getRightPadBottomY();
			Point currentPos = board.getBallPos();
			boolean leftIsDefender = direction == HorizontalDirection.LEFT;
			boolean rightIsDefender = !leftIsDefender;
			final int currentBallSpeed = ballSpeed;
			Collection<Callable<PadMovement>> movementTasks = Arrays.<Callable<PadMovement>>asList(
					() -> {
						return leftUser.nextStep(leftPadBottomY, rightPadBottomY, currentPos, currentBallSpeed, leftIsDefender);
					}, () -> {
						return rightUser.nextStep(rightPadBottomY, leftPadBottomY, currentPos, currentBallSpeed, rightIsDefender);
					});
			movePads(board, movementTasks);
			field.showMoves();
			for (int i = 0; i < ballSpeed; i++) {
				Point next = func.next(direction, currentPos);
				Collision collision = board.moveBall(next);
				if (collision == Collision.END) {
					field.moveBall(next);
					return direction;
				}
				// Refresh next should one of the coordinates have been out of bounds
				// (This can occur in the corners of the field or generally if the path
				// function is being recalculated twice in a row)
				next = board.getBallPos();
				HorizontalDirection oldDirection = direction;
				direction = collision.collide(func, direction, next);
				if (direction != oldDirection) {
					ballSpeed++;
				}
				field.moveBall(next);
				field.showMoves();
			}
		}
	}
	
	public void start() {
		if (leftUser == null || rightUser == null) {
			field.needUsersError();
			return;
		}
		field.reset();
		int leftVictories = 0;
		int rightVictories = 0;
		HorizontalDirection startDirection = START_DIRECTION;
		for (int halftime = 0; halftime < 2; halftime++) {
			for (int round = 0; round < HALFTIME_ROUNDS; round++) {
				HorizontalDirection finalDirection = startRound(startDirection);
				if (finalDirection == HorizontalDirection.LEFT) {
					field.incrementRightCounter();
					rightVictories++;
				} else {
					field.incrementLeftCounter();
					leftVictories++;
				}
			}
			field.displayHalftime();
			startDirection = startDirection.toggled();
		}
		if (rightVictories > leftVictories) {
			field.displayVictoryRight();
		} else if (leftVictories > rightVictories) {
			field.displayVictoryLeft();
		} else {
			field.displayDraw();
		}
	}

	public void setLeftUser(User leftUser) {
		this.leftUser = leftUser;
	}

	public void setRightUser(User rightUser) {
		this.rightUser = rightUser;
	}
	
}
